Day 21 的 RLVR 內容談及 final-answer verifier:答案對就給 1,答案錯就給 0;今天我們先不論訓練後的模型有沒有變強,讓我們延續訓練後留下的更新繼續探索,從同一題的 8 個 rollout 開始,經過 reward、advantage、response log probability,直到 GPU 上真的執行 backward 和 AdamW step。這樣的方法若能通過,就可確保模型在訓練過程中可接受到學習訊號,並更新權重;至於模型是否學會解題,就留待後續的訓練前後評估了。
本日仍使用固定版本的 Qwen/Qwen3-0.6B-Base,在 RTX 3090 Ti 上訓練。訓練集依原始順序取 50 題,每題依序抽 8 個 response,生成設定固定為 temperature 0.8、top-p 0.9,最多 512 tokens。每題完成一組 rollout 後只更新一次,因此整趟訓練共有 50 次參數更新、400 個 rollout。
訓練過程逐步留下了以下紀錄,值得注意的是每兩個步驟間的差異:
step correct/8 reward mean sample std response tokens policy loss grad norm
1 0 0.000 0.000000 1554 0.000000 0.000
2 1 0.125 0.353553 564 -3.042482 256.815
第 1 題生成了 1,554 個 response tokens,平均每個 response 194.25 tokens;8 個答案卻全錯。verifier 交回八個 0,組內沒有任何差異。文字很多,gradient 仍然是 0。第 2 題只出現一個正解,另外七個都錯,這一個 1 才把原本平的 reward 分布撐開。
程式沒有拿這個 1 直接乘上 loss,而是先在同一題的 8 個 rollout 之內標準化。實作使用 sample standard deviation,也就是分母採用 n - 1:
reward_mean = rewards.mean()
reward_std = rewards.std(unbiased=True)
advantages = (rewards - reward_mean) / (reward_std + 1e-4)
第 2 步的 reward 集合是 [1, 0, 0, 0, 0, 0, 0, 0](排列順序不影響計算)。它的平均值是 0.125,sample standard deviation 是 0.353553。加上預先固定的 1e-4 穩定項之後,正解得到約 2.4742 的 advantage,每個錯誤答案得到約 -0.3535。八個 advantage 加總仍為 0;正解被往上推,七個錯解各自受到較小的反向壓力。
這個標準化只在「同一題、同一組 8 個 rollout」內發生。第 2 題的 0.125 不會拿去跟第 1 題或後面 48 題比較,也沒有跨題移動平均當 baseline。advantage 表達的是某個 response 相對於同題其他候選的好壞,不是整份資料集中這一題有多難;代價則是難題若八次全錯,沒有較好的樣本可推;簡單題若八次全對,也沒有較差的樣本可壓。
這也解釋了第 1 步為什麼完全不動。八個 reward 都是 0 時,平均值和標準差也都是 0;穩定項可以避免除以零,卻無法憑空製造差異,因為每個分子仍是 0。若八個 response 全部答對,計算也會得到同樣的結果。RLVR 需要的不只是答對,而是同組 rollout 之間剛好出現可比較的對與錯。
有了 advantage,程式會把 prompt 與各自的 response 接回模型,重新計算 response token 的 next-token log probability。它從「預測第一個 response token」的位置開始取值,只保留 response 範圍,接著沿序列相加:
sequence_log_prob_i
= sum_t log p_theta(response_token_t | prompt, earlier_response_tokens)
這裡採用 sequence sum,沒有除以 response 長度。長 response 會累積更多 token log probabilities,這是本次預先固定的計算方式,不該在看到結果後偷偷換成 token mean。8 條序列的 policy loss 再寫成:
policy_loss
= -(1 / 8) * sum_i(stopgrad(advantage_i) * sequence_log_prob_i)
正 advantage 會提高那條 response 的機率,負 advantage 會降低對應錯誤 response 的機率。stopgrad 則把 reward 與標準化結果當成固定權重;backward 只沿著模型算出的 log probability 回傳。
生成 response 的動作本身是離散 sampling,無法直接對「抽中了哪個 token」微分。policy-gradient 的做法是保留抽出的 token IDs,再用目前的模型重算這些既定 token 的 log probability。reward 告訴 loss 要往哪個方向施力,log probability 提供能一路回到模型權重的可微路徑。這正是那個 1 從 verifier 進入神經網路的接點。
七個錯誤 response 雖然都拿到約 -0.3535,產生的梯度也不會一模一樣。每條 response 的 token、長度與條件機率不同,反向傳播經過的運算路徑自然不同;最後累積的是八組參數梯度的向量和。advantage 決定整條 trajectory 的正負與相對權重,response 內容決定這股力量實際落在哪些參數方向。
這次實作沿用 Raschka 原始範例的 no-KL 設定,沒有 reference model KL,也沒有 old-policy ratio 或 ratio clipping。這不是所有 GRPO 實作的通用配方;我們在訓練前便決定採用這套最小 RLVR 更新。每條 response 的 loss 先除以 8 再逐條 backward,8 次梯度累積完成後,以 max_grad_norm=1.0 裁切,最後用 learning rate 1e-5 執行 AdamW step。
逐條 backward 在數學上仍對應前式的八條序列平均,只是避免同時保留八份完整計算圖。程式在一組開始前先把舊梯度歸零,每條 response 完成 forward 後立刻把該條貢獻累積進參數;第八條結束才一起裁切與更新。optimizer 因此每題走一步,不是每個 rollout 各走一步。
第 2 步記錄的 gradient norm 是 256.815,這是 clip_grad_norm_ 回傳的裁切前總 norm,實際送進 optimizer 的梯度已受 1.0 上限約束。policy_loss=-3.042482 也不是「負分」:log probability 本來多半為負,乘上正負 advantage 後再加總,loss 的正負不能當成模型品質。這一列真正提供的證據,是非零 reward variance 產生了非零 gradient,而且參數更新確實被執行。
仔細檢視全部步驟,可發現 400 個 rollout 只有 15 個答對,平均 reward 是 0.0375;45 組的 reward variance 為 0,只有第 2、4、27、43、47 步出現非零梯度。大部分時間不是 optimizer 壞掉,而是同組 8 個 response 沒有形成可比較的結果。
第 2 步裡答對得到的 1 分確實讓權重更新了,但它仍把整段答對的 response 當成一個單位:同一個正 advantage 乘上序列中每個 token 的 log probability。verifier 知道最後答案正確,卻不知道哪一步推理值得保留、哪一句只是碰巧混在正解前面。RLVR 已經把結果送進 gradient,credit assignment 卻仍不夠細緻,這個問題是我們接下來繼續處理的。